﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using Phoenix.Frames;
using Phoenix.Framework;
using Phoenix.Utilities;

namespace Phoenix.Commands
{
    public static class Invoke
    {
        #region public static attached string OnClick
        public static readonly DependencyProperty OnClickProperty =
            DependencyProperty.RegisterAttached("OnClick", typeof(string), typeof(Invoke), new PropertyMetadata(default(string), OnClickChanged));

        public static void SetOnClick(ButtonBase element, string value)
        {
            element.SetValue(OnClickProperty, value);
        }

        public static string GetOnClick(ButtonBase element)
        {
            return (string)element.GetValue(OnClickProperty);
        }
        #endregion

        #region public static attached string OnItemSelected
        public static readonly DependencyProperty OnItemSelectedProperty =
            DependencyProperty.RegisterAttached("OnItemSelected", typeof(string), typeof(Invoke), new PropertyMetadata(default(string), OnItemSelectedChanged));

        public static void SetOnItemSelected(Selector element, string value)
        {
            element.SetValue(OnItemSelectedProperty, value);
        }

        public static string GetOnItemSelected(Selector element)
        {
            return (string)element.GetValue(OnItemSelectedProperty);
        }
        #endregion

        #region public static attached string OnSelection
        public static readonly DependencyProperty OnSelectionProperty =
            DependencyProperty.RegisterAttached("OnSelection", typeof(string), typeof(Invoke), new PropertyMetadata(default(string), OnSelectionChanged));

        public static void SetOnSelection(UIElement element, string value)
        {
            element.SetValue(OnSelectionProperty, value);
        }

        public static string GetOnSelection(UIElement element)
        {
            return (string)element.GetValue(OnSelectionProperty);
        }
        #endregion

        #region public static UIElement.InvokeOptions
        public static readonly DependencyProperty OptionsProperty =
            DependencyProperty.RegisterAttached("Options", typeof(InvokeOptions), typeof(Invoke), new PropertyMetadata(InvokeOptions.None));

        public static void SetOptions(UIElement element, InvokeOptions value)
        {
            element.SetValue(OptionsProperty, value);
        }

        public static InvokeOptions GetOptions(UIElement element)
        {
            if (element == null)
                return InvokeOptions.None;
            var value = element.GetValue(OptionsProperty);
            if (value == null)
                return InvokeOptions.None;
            return (InvokeOptions)value;
        }

        #endregion

        #region public static attached IContentFrame TargetFrame
        public static readonly DependencyProperty TargetFrameProperty =
            DependencyProperty.RegisterAttached("TargetFrame", typeof(IContentFrame), typeof(Invoke), new PropertyMetadata(default(IContentFrame)));

        public static void SetTargetFrame(UIElement element, IContentFrame value)
        {
            element.SetValue(TargetFrameProperty, value);
        }

        public static IContentFrame GetTargetFrame(UIElement element)
        {
            return (IContentFrame)element.GetValue(TargetFrameProperty);
        }
        #endregion

        #region public static attached string TargetFrameName
        public static readonly DependencyProperty TargetFrameNameProperty =
            DependencyProperty.RegisterAttached("TargetFrameName", typeof(string), typeof(Invoke), new PropertyMetadata(default(string)));

        public static void SetTargetFrameName(UIElement element, string value)
        {
            element.SetValue(TargetFrameNameProperty, value);
        }

        public static string GetTargetFrameName(UIElement element)
        {
            return (string)element.GetValue(TargetFrameNameProperty);
        }
        #endregion

        //Used so we don't setup the listener multiple times
        #region private static attached bool ListenForItemSelection
        // ReSharper disable InconsistentNaming
        private static readonly DependencyProperty ListenForItemSelectionProperty =
            DependencyProperty.RegisterAttached("ListenForItemSelection", typeof(bool), typeof(Invoke), new PropertyMetadata(default(bool)));

        // ReSharper restore InconsistentNaming

        private static void SetListenForItemSelection(UIElement element, bool value)
        {
            element.SetValue(ListenForItemSelectionProperty, value);
        }

        private static bool GetListenForItemSelection(UIElement element)
        {
            return (bool)element.GetValue(ListenForItemSelectionProperty);
        }
        #endregion

        private static void OnSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var element = d as FrameworkElement;
            if (element == null) return;
            RoutedEventHandler elementOnLoaded = null;
            // ReSharper disable AccessToModifiedClosure
            elementOnLoaded = (sender, args) =>
            {
                element.Loaded -= elementOnLoaded;
                var selector = element.Ancestors<Selector>().FirstOrDefault();
                if (selector == null) return;

                //Ignore setup after first item
                if (GetListenForItemSelection(selector))
                    return;

                SetListenForItemSelection(selector, true);
                SetupSelectionListner(selector, () =>
                                                {
                                                    var command = GetOnItemSelected(selector);
                                                    var controllerAction = ControllerAction.CreateFrom(command);
                                                    return string.Format("InvokeAction.OnItemSelected=\"{0}\"", controllerAction);
                                                });
            };
            // ReSharper restore AccessToModifiedClosure
            element.Loaded += elementOnLoaded;
        }

        private static void OnItemSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var selector = (Selector)d;
            RoutedEventHandler selectorOnLoaded = null;
            // ReSharper disable AccessToModifiedClosure
            selectorOnLoaded = (sender, args) =>
            {
                selector.Loaded -= selectorOnLoaded;
                SetupSelectionListner(selector, () =>
                {
                    var command = GetOnSelection(selector);
                    var controllerAction = ControllerAction.CreateFrom(command);
                    return string.Format("InvokeAction.OnSelection=\"{0}\"", controllerAction);
                });
            };
            // ReSharper restore AccessToModifiedClosure
            selector.Loaded += selectorOnLoaded;
        }

        static void SetupSelectionListner(Selector selector, Func<string> getAttachedPropertyCode)
        {
            var viewModel = GetViewModel(selector);

            if (viewModel == null)
            {
                Debug.WriteLine("Cannot find a DataContext of type ViewModelBase in visual tree to bind {0}", getAttachedPropertyCode());
                return;
            }

            viewModel.AddSetupTask(() => selector.SelectionChanged += SelectorSelectionChanged);
            viewModel.AddCleanupTask(() => selector.SelectionChanged -= SelectorSelectionChanged);
        }

        private static void OnClickChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var command = (string)e.NewValue;
            var controllerAction = ControllerAction.CreateFrom(command);

            if (e.NewValue == DependencyProperty.UnsetValue)
                d.ClearValue(ButtonBase.CommandProperty);
            else if (controllerAction != null)
            {
                var delegateCommand = new AsyncDelegateCommand(() => NavigateCommand((ButtonBase) d, controllerAction));

                d.SetValue(ButtonBase.CommandProperty, delegateCommand);
            }
        }

        private static void SelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var selector = (Selector)sender;
            if (selector.SelectedItem == null) return;
            var selectedUiItem = selector.SelectedItem as UIElement;

            // Try selected item first, then fall back to selector
            var command = (selectedUiItem == null ? null : GetOnSelection(selectedUiItem)) ?? GetOnItemSelected(selector);
            if (command == null) return;
            var controllerAction = ControllerAction.CreateFrom(command);

            if (e.AddedItems.Count != 1) return;
            var viewModel = GetViewModel(selector);
            if (viewModel == null)
            {
                Debug.WriteLine("Cannot find a DataContext of type ViewModelBase in visual tree to bind InvokeAction.OnSelection or OnItemSelected");
                return;
            }

            string assembly;
            string controller;
            string action;
            KeyValuePair<string, string>[] arguments;
            if (ControllerAction.ParseCommand(command, out assembly, out controller, out action, out arguments))
            {
                var dataContext = e.AddedItems[0];
                if (dataContext is ListBoxItem)
                    dataContext = ((ListBoxItem)dataContext).DataContext;

                try
                {
                    var targetFrame = GetTargetFrame(selector, selectedUiItem, viewModel);

                    if (arguments == null && (dataContext == null || dataContext is ViewModelBase))
                        viewModel.InvokeActionBuilder.InvokeAction(targetFrame, controllerAction, options: GetOptions(selectedUiItem));
                    else if (arguments == null)
                    {
                        viewModel.InvokeActionBuilder.InvokeAction(targetFrame, controllerAction,
                            new[] { dataContext }, options: GetOptions(selectedUiItem));
                    }
                    else if (dataContext == null || dataContext is ViewModelBase)
                    {
                        viewModel.InvokeActionBuilder.InvokeAction(targetFrame, controllerAction,
                            new object[] { arguments }, options: GetOptions(selectedUiItem));
                    }
                    else //Both possible arguments are not null
                    {
                        viewModel.InvokeActionBuilder.InvokeAction(targetFrame, controllerAction,
                            new[] { dataContext, arguments }, options: GetOptions(selectedUiItem));
                    }
                }
                catch (Exception ex)
                {
                    viewModel.Context.Host
                        .ErrorReporter
                        .ReportException(ex, string.Format("Triggered by InvokeAction.OnItemSelected=\"{0}\"", controllerAction), false);
                }
            }
            selector.SelectedIndex = -1;
        }

        static IContentFrame GetTargetFrame(UIElement selector, UIElement selectedUiItem, ViewModelBase viewModel)
        {
            var contentFrame = (selectedUiItem == null ? null : GetTargetFrame(selectedUiItem)) ?? GetTargetFrame(selector);

            if (contentFrame == null)
            {
                var contentFrameName = (selectedUiItem == null ? null : GetTargetFrameName(selectedUiItem)) ?? GetTargetFrameName(selector);
                if (contentFrameName != null)
                {
                    // Best effort to find content frame
                    contentFrame = viewModel.Context.ResolveFrame("contentFrameName");
                }
            }

            return contentFrame;
        }

        private static Task NavigateCommand(FrameworkElement button, ControllerAction controllerAction)
        {
            var viewModel = GetViewModel(button);

            if (viewModel == null) return null;

            var dataContext = button.DataContext;
            var targetFrame = GetTargetFrame(button);

            //Only pass datacontext if it isn't a viewmodel
            if (dataContext is ViewModelBase)
                return viewModel.InvokeActionBuilder.InvokeAction(targetFrame, controllerAction, options: GetOptions(button));
            
            return viewModel.InvokeActionBuilder.InvokeAction(targetFrame, controllerAction, new[] { dataContext },
                                                              options: GetOptions(button));
        }

        private static ViewModelBase GetViewModel(DependencyObject d)
        {
            var viewModel = d.AncestorsAndSelf()
                .OfType<FrameworkElement>()
                .Select(a => a.DataContext)
                .OfType<ViewModelBase>()
                .FirstOrDefault();

            return viewModel;
        }
    }
}